<?php

class XML_Tree {
  public $data        =  array();
  public $source      =  '';
  
  protected function mkxml( &$ob, $depth=0 ) {
    if ( is_string($ob) ) {
      return $ob."\n";
    }
    if ( $ob === null || !array_key_exists( '_name', $ob ) ) {
      return '';
    }
    $indent = $this->indent( $depth );

    $res = '';
    $res .= $indent . '<' . $ob['_name'];
    foreach ( $ob as $k => $val ) {
      if ( $k != '_name' && $k != '_data' ) {
        $res .= ' ' . $k . '="' . $val . '"';
      }
    }
    if ( empty( $ob['_data'] ) && $ob['_data']!==0 ) {
      $res .= " />\n";
    } else {
      $res .= '>';
      if ( is_array($ob['_data']) ) {
        $res .= "\n";
        foreach ( $ob['_data'] as $o  )
            $res .= $this->mkxml( $o, $depth+1 );
        $res .= $indent;
      } else {
        $res .= $ob['_data'];
      }
      $res .= '</' . $ob['_name'] . ">\n";
    }
    return $res;
  }

  protected function indent( $n ) {
    $res = '';
    for ( ; $n>0 ; --$n, $res.='  ' );
    return $res;
  }
  
  
}


class XML extends XML_Tree {
  private $splitter   =  array(' ', "\n", "\t", "\r", ">");
  public $version     =  '1.0';
  public $encoding    =  'UTF-8';
  public $meta        =  '';
  public $pos         =  0;
  public $stack       =  array();
  
  public $error       =  array();

  public function __get( $var ) {
    return $var;
  }

  public function __construct( $src=null ) {
    if ( is_null($src) ) {
      return;
    }
    if ( is_array($src) ) {
      $this->data = $src;
    }
    if ( is_string($src) ) {
      $this->source = $src;
    }
  }

  /**
   * Read one word from $source
   * @return string
   */
  public function read( $quotes=true ) {
    $res = '';
    $quoted = false;
    $cdata  = false;
    for ( ; isset($this->source{$this->pos}) && in_array($this->source{$this->pos},$this->splitter)
    ; $this->pos++ );
    for ( ; isset($this->source{$this->pos}) && (
    $quoted || $cdata || ( !in_array($this->source{$this->pos},$this->splitter) &&
    ( $res=='' || $this->source{$this->pos}!='<' ) )
    )
    ; $this->pos++ ) {
      $res .= $this->source{$this->pos};
      if ( $quotes && $this->source{$this->pos} == '"' ) {
        $quoted = !$quoted;
      }
      if ( !$quoted && !$cdata && substr($this->source,$this->pos,9)=='<![CDATA[' ) {
        $cdata = true;
      }
      if ( !$quoted && $cdata && substr($this->source,$this->pos,3)==']]>' ) {
        $cdata = false;
      }
    }
    return $res;
  }


  function remove_quotes( $str ) {
    if ( is_string( $str ) ) {
      $l = strlen( $str );
      if ( $str{0}=='"' && $str{$l-1}=='"' ) {
        return substr( $str, 1, $l-2 );
      }
    }
    return $str;
  }

  /**
   * Open tag
   * @param $tag
   * @return void
   */
  private function open( $tag ) {
    array_push( $this->stack, $tag );
  }


  /**
   * Close $tag
   * @param $tag
   * @return bool
   */
  private function close( $tag=null ) {
    $t = array_pop($this->stack);
    if ( is_null($tag) || ($tag == $t) ) {
      return true;
    }
    array_push( $this->stack, $t );
    $this->raise( 2, $tag );
    return false;
  }


  /**
   * parse tag attributes
   * @return array
   */
  private function parse_attr() {
    $res = array();
    while ( $at = $this->read() ){
      $t = explode('=',$at,2);
      if ( empty($t[1]) ) $t[1] = 1;
      if ( $t[0] != '/' ) {
        if ( preg_match( '/[~!`#&<()?*^%;\\|\/,]/', $t[0], $m ) ) {
          $this->raise( 4, $m[0] );
        }
        $res[$t[0]] = $this->remove_quotes( $t[1] );
      }
      if ( empty($at) || $this->source[$this->pos] == '>') {
        break;
      }
    }
    if ( $this->source{$this->pos-1} == '/' ) {
      $this->close();
    } else {
      $res['_data'] = array();
    }
    return $res;
  }


  /**
   * Read tag from $source
   * @return array
   */
  private function parse_tag() {
    $p = $this->pos+1;
    $tag = $this->read(false);
    if ( $tag == '' ) {
      return false;
    }

    if ( substr($tag,0,9) == '<![CDATA[' ) {
      $tag .= '>';
    } elseif ( $tag{0} == '<'  ) {
      if ( $tag{1} == '/' ) { 
        if ( $this->source[$this->pos] == '>' ) {
          $this->close(substr($tag, 2));
          return false;
        } else {
          $this->raise( 3 );
          return '';
        }
      } elseif ( $this->source[$this->pos] == '>' ) {
        // tag has no attributes
        if ( $this->source[$this->pos-1] == '/' ) {
          // tag closed, no data inside
          return array( '_name' => substr($tag, 1, strlen($tag)-2) );
        } else {
          // parse some data inside
          $tag = array( '_name' => substr($tag, 1) );
          $this->open($tag['_name']);
          for ( $i = 0 ; !is_bool( $tmp = $this->parse_tag() ) ; $i++ ) {
            $tag['_data'][$i] = $tmp;
          }
          if ( isset($tag['_data']) && !isset($tag['_data'][1]) && is_string($tag['_data'][0]) ) {
            $tag['_data'] = $tag['_data'][0];
          }
        }

      } else {
        // need to scan attributes
        $tag = array( '_name' => substr($tag, 1) );
        $this->open($tag['_name']);
        $c = count($this->stack)-1;
        $tag = array_merge($tag, $this->parse_attr());
        if ( isset($this->stack{$c}) && $this->stack{$c}==$tag['_name'] ) {
          for ( $i = 0 ; !is_bool( $tmp = $this->parse_tag() ) ; $i++ ) {
            $tag['_data'][$i] = $tmp;
          }
        }
        if ( !empty($tag['_data']) && !isset($tag['_data'][1]) && is_string($tag['_data'][0]) ) {
          $tag['_data'] = $tag['_data'][0];
        }
      }
    } else {
      $tag = substr( $this->source, $p );
      $this->pos = strpos( $this->source, '<', $this->pos );
      if ( $this->pos === false ) $this->pos = strlen( $this->source ) - 1;
      $tag = substr( $tag, 0, strpos($tag, '<'));
    }
    //var_dump($tag);
    return $tag;
  }


  /**
   * Parse $src if set or $source
   * @param [opt]text $src
   * @return void
   */
  public function parse( $src=null ) {
    if ( $src !== null ) {
      $this->source = $src;
    }
    $this->meta     = '';
    $this->version  = '1.0';
    $this->encoding = 'UTF-8';
    preg_match_all( '/<\?xml[^(\?>)]*\?>/', $this->source, $matches );
    $fl = false;
    if ( isset($matches[0]) && is_array($matches[0])) foreach( $matches[0] as $m ) {
      if ( preg_match( '/^<\?xml\s+version="(1\.1|1\.0)"\s+encoding="([A-Za-z0-9\-]+)"\s*\?>$/', $m, $d) ) {
        $this->version = $d[1];
        $this->encoding = $d[2];
        $fl = true;
      } else {
        $this->meta .= $m . "\n";
      }
    }
    if ( !$fl ) {
      $this->raise( 5 );
    }
    $this->source = preg_replace( '/<!DOCTYPE[^>]*>/',     '',  $this->source );
    $this->source = preg_replace( '/<!--(?(?<=-)[^-]|.)*-->/',        '',  $this->source );
    $this->source = preg_replace( '/<\?xml[^(\?>)]*\?>/',  '',  $this->source );
    if ( preg_match( '/<!--(?(?<=--)[^>]|.)*-->/', $this->source ) ) {
      $this->source = preg_replace( '/<!--(?(?<=--)[^>]|.)*-->/',  '',  $this->source );
      $this->raise( 6 );
    }
    $this->pos    = 0;
    $this->data   = $this->parse_tag();
    while ( $v = array_pop($this->stack) ) {
      $this->raise( 1, $v );
    }
  }

  

  /**
   * Build XML code from $data;
   * @return void
   */
  public function build() {
    $this->source  = '<?xml version="' . $this->version . '" encoding="' . $this->encoding . "\"?>\n";
    $this->source .= $this->meta;
    $this->source .= $this->mkxml( $this->data );
    return;
  }



  /**
   * Load $source from file. true on success
   * @param string $file
   * @return bool
   */
  public function load( $file ) {
    if ( file_exists($file) ) {
      $this->source = implode( '', file($file) );
      return true;
    }
    return false;
  }

  
  
  /**
   * Build XML and save to file.   true on success
   * @param string $file
   * @return bool
   */
  public function save( $file ) {
    $f = fopen( $file, "w" );
    if ( $f ) {
      $this->build();
      fputs( $f, $this->source );
      fclose( $f );
      return true;
    }
    return false;
  }
  
  
  
  /**
   * Generate error text from $error code and
   * @param int $error
   * @param string[opt] $parm
   * @return unknown_type
   */
  private function errortext( $error, $parm='' ) {
    switch ( $error ) {
      case 0:
          return '';
      case 1:
          return 'End of tag "' . $parm . '" expected.';
      case 2:
          return 'Unexpected end of tag "' . $parm . '".';
      case 3:
          return 'Expected ">"';
      case 4:
          return 'Unallowed symbol in attribute name: "' . $parm . '".';
      case 5:
          return 'No XML metadata (<?xml ?>).';
      case 6:
          return 'Multiple dash inside comment.';
      case 7:
          return 'Unallowed symbol in tag name: "' . $parm . '".';
      default:
          return 'Unknown error';
    }
  }
  
  private function raise( $error, $parm='' ) {
    $p = $this->pos;
    if ( $p === 0 ) $p++;
    $hay = substr_count( $this->source, "\n", 0, $p ) + 1;
    array_push( $this->error, $hay.': '.$this->errortext( $error, $parm ) );
  }
  
  public function root( $name, $data=null ) {
    $this->data = array( '_name' => $name, '_data' => $data );
  }
   

  /**
   * Find elements in XML tree
   * @param string $q
   * @return XML_Fragment
   */
  public function find( $q=null ) {
    if ( isset($q) ) {
      $q = explode( ' ', $q );
      foreach ( $q as &$t ) {
        $tmp = array();
        while ( $n = strpos( $t, '[' ) ) {
          $m = strpos( $t, ']' );
          list($k,$v) = explode( '=', substr( $t, $n+1, $m-$n-1 ) );
          $tmp[$k] = $v; 
          $t = substr( $t, 0, $n ) . substr( $t, $m+1 );
        }
        $tmp['_name'] = $t;
        $t = $tmp;
      }
      return new XML_Fragment( $this->search( $q, $this->data ) );
    }
    $t = array( &$this->data );
    return new XML_Fragment( $t );
  }

  
  /**
   * Logics of find($q) method
   * @param array $q
   * @param array $obj
   * @return array
   */
  private function search( $q, &$obj ) {
    if ( empty($q) ) {
      return $obj;
    }
     
    if ( $this->match( $q[0], $obj ) ) {
      array_shift( $q );
    }
    if ( empty( $q ) ) {
      return array( &$obj );
    } else {
      if ( isset( $obj['_data'] ) && is_array( $obj['_data'] ) ) {
        $t = array();
        foreach ( $obj['_data'] as &$ob ) {
          $t = array_merge( $t, $this->search( $q, $ob ) );
        }
        return $t;
      }
      if ( isset( $obj['_data'] ) && empty($q) )  {
        return array( &$obj );
      }
    }
    return array();
  }
  
  public  function match( &$q, &$obj ) {
    if ( is_array($obj) && array_key_exists( '_name', $obj ) ) {
      $fl = true;
      foreach ( $q as $k => $v ) {
        if ( ($k !== '_name' && $v !== '*') || $obj[$k] !== $v ) {
          $fl = false;
        }
      }
      return $fl;
    }
    return false;
  }
  
}



/**
 * 
 * @author silentium
 *
 */
class XML_Fragment extends XML_Tree {
  private $cur = 0;
  
  public function __construct( &$arr ) {
    $this->data = &$arr;
  }
  
 
  /**
   * Get subtree from element on current position and switch to next
   * @return XML
   */
  public function &get_tree () {
    // результаты закончились
    if ( !array_key_exists( $this->cur, $this->data ) || empty($this->data[$this->cur]) ) {
      $t =  false;
      return $t;
    }
    // вытаскиваем значение
    $res = new XML();
    $res->data = &$this->data[$this->cur++];
    return $res;
  }
  
   
  /**
   * Get data from element on current position and switch to next
   * @return mixed
   */
  public function get () {
    // результаты закончились
    if ( !array_key_exists( $this->cur, $this->data ) ) {
      return false;
    }
    // значение пусто
    if ( is_null( $this->data[$this->cur] ) || !array_key_exists( '_data', $this->data[$this->cur] ) ) {
      ++$this->cur;
      return null;
    }
    // вытаскиваем значение
    $res = '';
    if ( is_array( $this->data[$this->cur]['_data'] ) ) {
      foreach ( $this->data[$this->cur]['_data'] as &$piece ) {
        $res .= $this->mkxml( $piece );
      }
    } else {
      $res = $this->data[$this->cur]['_data'];
    }
    ++$this->cur;
    return $res;
  }
  
  
  /**
   * Get array of elements' data
   * @return array
   */
  public function get_all() {
    $this->cur = 0; // начинаем с начала
    $res = array();
    while ( array_key_exists( $this->cur, $this->data ) ) { // пока есть элементы
      $res[$this->cur] = $this->get();   // возьмем по одному - получим все
    }
    $this->cur = 0; // вернемся в начало
    return $res;
  }
  
  
  
  /**
   * Set element data with $value
   * @param mixed $value
   * @return void
   */
  public function set( $value ) {
    if ( !isset( $this->data[$this->cur] ) ) {
      return false;
    }
    $this->data[$this->cur]['_data'] = $value;
  }
   
  
  /**
   * Get link to $at attribute value if $at is set 
   * or link to assoc array of attributes 
   * @param string[opt] $at   sought-for attribute
   * @return link/bool
   */
  public function &attr( $at=null, $value=null ) {
    if ( !array_key_exists( $this->cur, $this->data ) ) {
      $t = null;
      return $t;
    }
    if ( isset($value) ) {
      $this->data[$this->cur][$at] = $value;
      $t = true;
      return $t;
    }
    if ( is_null( $at ) ) {
      return $this->data[$this->cur];
    }
    if ( !array_key_exists( $at, $this->data[$this->cur] ) ) {
      $t = false;
      return $t;
    }
    return $this->data[$this->cur][$at];
  }
  
  
  /**
   * delete element on current position
   * @return bool
   */
  public function delete() {
    if ( array_key_exists( $this->cur, $this->data ) ) {
      $i = $this->cur;
      for ( ; array_key_exists( $i+1, $this->data ) ; $i++ ) {
        $this->data[$i] = $this->data[$i+1];
      }
      $this->data[$i] = null;
      return true;
    }
    return false;
  }
  
  
  /**
   * Add new XML element inside current
   * @param string $tag
   * @param string[opt] $value
   * @return XML_Fragment
   */
  public function add( $tag, $value=null ) {
    if ( !array_key_exists( $this->cur, $this->data ) ) {
      return false;
    }
    if ( is_null($this->data[$this->cur]['_data']) ) {
      $this->data[$this->cur]['_data'] = array();
    }
    if ( is_string($this->data[$this->cur]['_data']) ) {
      $this->data[$this->cur]['_data'] = array( $this->data[$this->cur]['_data'] );
    }
    array_push( $this->data[$this->cur]['_data'], array( '_name' => $tag, '_data' => $value ) );
    $t = array( &$this->data[$this->cur]['_data'][count($this->data[$this->cur]['_data'])-1] );
    return new XML_Fragment( $t );
    
  }
  
  
  /**
   * delete all found elements
   * @return void
   */
  public function delete_all() {
    for ( $i = 0 ; array_key_exists( $i, $this->data ) ; $i++ ) {
      $this->data[$i] = null;
    }
  }
  
  
  /**
   * Go to given position or return to firsrt
   * @return void
   */
  public function go( $pos=0 ) {
    if ( !array_key_exists( $pos, $this->data ) ) $pos = 0;
    $this->cur = $pos+0;
  }
  

  
  /**
   * Number of pointed xml tree fragments
   * @return unknown_type
   */
  public function num() {
    return count($this->data);
  }
  
}

?>